/**
 * Engine implementation
 */

#include "Engine.h"
#include "Shader.h"
#include "FloatTexture.h"
#include "ByteTexture.h"

#include "shared.h"

/////////////////////////////////////////////////

Engine::Engine() : _size(0,0), _numSecLights(LIGHTS_PER_PASS), _radOn(true), _showFromLight(false)
{
}

/////////////////////////////////////////////////

void Engine::init(const Vec2s& size)
{
	_size = size;
	// TODO only lighting back side?
	// TODO resize everything when the screen resizes? 
	std::cerr << "Engine init." << std::endl;
	glClearColor(0.2, 0.2, 0.3, 1.0);
	glDepthFunc(GL_LEQUAL);
	glCullFace(GL_BACK);
	glEnable(GL_CULL_FACE);

	// Generate the halton points
	_secLightIndexes = new Vec2f[_numSecLights];
	
	for (unsigned int j=0; j<LIGHTS_PER_PASS; ++j) {
		_secLightIndexes[j] = Vec2f(phiHalton(2, j)*_size[0], phiHalton(3, j)*_size[1]);
	}
#if 0
	// TODO this makes somewhat of a fps difference, but minimal
	for (int x=0; x<10; x++) {
		for (int y=0; y<10; y++) {
			_secLightIndexes[x*10+y] = Vec2f(y,x);
		}
	}
#endif

	// Load shaders
	std::cerr << "Loading shaders." << std::endl;
	// Deferred shaders
	ProgramPtr dsProgram(new Program("deferred shader ec", "./src/ds.vert", "./src/ds.frag"));
	_dsProg = dsProgram;
	
	ProgramPtr pProgram(new Program("phong", "./src/phong.vert", "./src/phong.frag"));
	_defaultProgram = pProgram;
	_defaultProgram->load();
	_defaultProgram->setSampler("dsEC", 0);
	_defaultProgram->setSampler("dsNorm", 1);
	_defaultProgram->setSampler("dsColor", 5);
	_defaultProgram->unload();

	ProgramPtr sProgram(new Program("sec light phong", "./src/secPhong.vert", "./src/secPhong.frag"));
	_secLightProgram = sProgram;
	_secLightProgram->load();
	_secLightProgram->setSampler("dsEC", 0);
	_secLightProgram->setSampler("dsNorm", 1);
	_secLightProgram->setSampler("lightPos", 2); 
	_secLightProgram->setSampler("lightDir", 3); 
	_secLightProgram->setSampler("lightColor", 4); 
	_secLightProgram->setSampler("dsColor", 5);
	assert(_numSecLights);
	_secLightProgram->setVec2Array("lightIndex", LIGHTS_PER_PASS, _secLightIndexes);
	_secLightProgram->unload();

	// From light shaders
	ProgramPtr fromLightProgram(new Program("from light", "./src/fromLight.vert", "./src/fromLight.frag"));
	_fromLightProg = fromLightProgram;
	std::cerr << "Shaders loaded." << std::endl;

	// Create render textures and framebuffers 
	std::cerr << "Creating render targets." << std::endl;
	FramebufferPtr fboFromLight(new Framebuffer("framebuffer object from light"));
	FramebufferPtr fboDS(new Framebuffer("framebuffer object ds"));
	FramebufferPtr fboFinal(new Framebuffer("framebuffer object final"));
    RenderbufferPtr rb(new Renderbuffer("depth buffer", _size,
                                        GL_DEPTH_ATTACHMENT_EXT, GL_DEPTH_COMPONENT));
	_fboFromLight = fboFromLight;
	_fboDS = fboDS;
	_fboFinal = fboFinal;

	// TODO Using byte textures here is faster, but obviously less precise 
	TexturePtr texNorm(new FloatTexture("normal texture", _size));
	TexturePtr texEC(new FloatTexture("ec texture", _size));
	TexturePtr texColor(new FloatTexture("color texture", _size));
	_fromLight[0] = texEC;
	_fromLight[1] = texNorm;
	_fromLight[2] = texColor;

	//TexturePtr standard(new FloatTexture(std::string("standard"), _size));
	// TODO verify this does not cause any artifacts, etc.
	TexturePtr standard(new ByteTexture(std::string("standard"), _size));
	_standard = standard;

	TexturePtr ds0(new FloatTexture("deferred shader results ec", _size));
	TexturePtr ds1(new FloatTexture("deferred shader results norm", _size));
	TexturePtr ds2(new FloatTexture("deferred shader results color", _size));
	_ds[0] = ds0;
	_ds[1] = ds1;
	_ds[2] = ds2;

	_fboFromLight->bind();
	_fboFromLight->attachRenderbuffer(rb);
	_fboFromLight->attachTexture(_fromLight[0], GL_COLOR_ATTACHMENT0_EXT);
	_fboFromLight->attachTexture(_fromLight[1], GL_COLOR_ATTACHMENT1_EXT);
	_fboFromLight->attachTexture(_fromLight[2], GL_COLOR_ATTACHMENT2_EXT);
	_fboFromLight->unbind();

	_fboDS->bind();
	_fboDS->attachRenderbuffer(rb);
	_fboDS->attachTexture(_ds[0], GL_COLOR_ATTACHMENT0_EXT);
	_fboDS->attachTexture(_ds[1], GL_COLOR_ATTACHMENT1_EXT);
	_fboDS->attachTexture(_ds[2], GL_COLOR_ATTACHMENT2_EXT);
	_fboDS->unbind();

	_fboFinal->bind();
	_fboFinal->attachTexture(standard, GL_COLOR_ATTACHMENT0_EXT);
	_fboFinal->unbind();
	
	std::cerr << "Render targets created." << std::endl;

	checkGLError();
}

/////////////////////////////////////////////////

void Engine::resize(const Vec2s& size)
{
	GLuint w = size[0];
	GLuint h = size[1];
	glViewport(0,0,w,h);
	/*
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(75.0, w/(GLfloat)(h==0 ? 1 : h), 0.1, 100.0);
	*/
	_size = size;
}

/////////////////////////////////////////////////

void Engine::render() 
{
	Camera realCam(Vec3(-2.78, 2.73, 6.50), Vec3(0.0, 0.0, -1.0), Vec3(0.0, 1.0, 0.0), 45.0);

	if (_showFromLight) {
		glEnable(GL_DEPTH_TEST);
		renderFromLight(_scene->getAreaLight(0), realCam);
		checkGLError();
		_fromLight[2]->drawFullScreen();
		// Overlay the halton points
		glDisable(GL_DEPTH_TEST);
		glPointSize(5.0);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		gluOrtho2D(0.0, _size[0], 0.0, _size[1]);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glColor3f(1.0, 1.0, 0.0);
		glBegin(GL_POINTS);
		for (unsigned int j=0; j<LIGHTS_PER_PASS; ++j) {
			glVertex3f(phiHalton(2, j)*_size[0], phiHalton(3,j)*_size[1], 0.5);
			glVertex3f(_secLightIndexes[j][0], _secLightIndexes[j][1], 0.5);
		}
		glEnd();
	} else {
		if (_radOn) {
			// Allows measuring the final pass speed
			// by only computing the first passes once
			static bool firstTime = true;
			if (firstTime) {
				//firstTime = false;
				// First, render from the light to grab some information...
				glEnable(GL_DEPTH_TEST);
				renderFromLight(_scene->getAreaLight(0), realCam);
				checkGLError();
				// Next, create the deferred shaders textures
				renderDS(_scene->getAreaLight(0), realCam);
				checkGLError();
			}
			// Final, combine the light contributions
			glDisable(GL_DEPTH_TEST);
			renderFinal(true);
			checkGLError();
		} else {
			// No radiosity
			glEnable(GL_DEPTH_TEST);
			renderDS(_scene->getAreaLight(0), realCam);
			checkGLError();
			glDisable(GL_DEPTH_TEST);
			renderFinal(false);
			checkGLError();
		}
	}
}

/////////////////////////////////////////////////

void Engine::renderFromLight(AreaLightPtr l, const Camera& realCam)
{
	// Camera from light
	Camera c(l->getCenter(), l->getDir(), l->getUp(), l->getFov());
	_fromLightProg->load();
	_fboFromLight->bind();
	//Matrix m = Matrix::inverse(c.getViewMatrix()) * realCam.getViewMatrix();
	Matrix m = realCam.getViewMatrix();
	_fromLightProg->setMatrix("toCameraEC", m);
	_fboFromLight->drawColorAttachments(3);
	glClear(GL_DEPTH_BUFFER_BIT);
	drawEntities(l, c);
	_fboFromLight->unbind();
	_fromLightProg->unload();
}

/////////////////////////////////////////////////

void Engine::renderDS(AreaLightPtr l, const Camera& c)
{
	_dsProg->load();
	_fboDS->bind();
	_fboDS->drawColorAttachments(3);
	glClear(GL_DEPTH_BUFFER_BIT);
	drawEntities(l, c);
	_fboDS->unbind();
	_dsProg->unload();
}

/////////////////////////////////////////////////

void Engine::renderFinal(bool rad)
{
	// TODO better scales?
	_defaultProgram->load();
	_fboFinal->bind();
	_defaultProgram->setFloat("lightScale", 1.0);
	_ds[0]->bind(GL_TEXTURE0);
	_ds[1]->bind(GL_TEXTURE1);
	_ds[2]->bind(GL_TEXTURE5);
	_fboFinal->drawBuffer(GL_COLOR_ATTACHMENT0_EXT);
	drawFullScreenQuad();
	_fboFinal->unbind();
	_defaultProgram->unload();

	if (rad) {
		// Now, add in the secondary lights
		_secLightProgram->load();
		_ds[0]->bind(GL_TEXTURE0);
		_ds[1]->bind(GL_TEXTURE1);
		_fromLight[0]->bind(GL_TEXTURE2);
		_fromLight[1]->bind(GL_TEXTURE3);
		_fromLight[2]->bind(GL_TEXTURE4);
		_ds[2]->bind(GL_TEXTURE5);
		_secLightProgram->setSampler("prevImage", 6); 
		_secLightProgram->setFloat("lightScale", 3.5/_numSecLights);
		_standard->bind(GL_TEXTURE6);
		glClear(GL_COLOR_BUFFER_BIT);
		// TODO allow changes in the number of secondary lights
		drawFullScreenQuad();
		_secLightProgram->unload();
	} else {
		_standard->drawFullScreen();
	}

}

/////////////////////////////////////////////////

void Engine::drawEntities(AreaLightPtr l, const Camera& c)
{
	assert(_scene != 0);
	assert(l != 0);

	static bool firstTime = true;
	static GLuint list;

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(c.getFovy(), _size[0]/(GLfloat)(_size[1]==0 ? 1 : _size[1]), 0.1, 100.0);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	Vec3 pos = c.getPos();
	Vec3 at = c.getAt();
	Vec3 up = c.getUp();
	gluLookAt(pos[0], pos[1], pos[2], at[0], at[1], at[2], up[0], up[1], up[2]); 
	glEnable(GL_LIGHT0);
	pos = l->getCenter();
	GLfloat lPos[] = { pos[0], pos[1], pos[2], 1.0};
	glLightfv(GL_LIGHT0, GL_POSITION, lPos);

	if (firstTime) {
		firstTime = false;
		list = glGenLists(1);

		glNewList(list, GL_COMPILE);
		for (unsigned int i=0; i<_scene->numEntities(); ++i) {
			EntityPtr e = _scene->getEntity(i);
			ModelPtr m = e->getModel();
			drawModel(m);
		}
		glEndList();
	}
	glCallList(list);
	// Needed?
	glDisable(GL_LIGHT0);

#if 0
	Program::useProgram();
	for (unsigned int i=0; i<_scene->numAreaLights(); ++i) {
		drawLight(_scene->getAreaLight(i));
	}
#endif
}

/////////////////////////////////////////////////

void Engine::drawModel(ModelPtr m)
{
	assert(m != 0);
	
	for (unsigned int p=0; p<m->parts.size(); ++p) {
		PartPtr part(m->parts[p]);		
		glMaterialfv(GL_FRONT, GL_DIFFUSE, part->mat.diffuse.ptr());
		glBegin(GL_TRIANGLES);
		for (unsigned int i=0; i<part->tris.size(); ++i) {
			Triangle t = part->tris[i];
			Vec3 v0 = part->verts[t.v[0]];
			Vec3 v1 = part->verts[t.v[1]];
			Vec3 v2 = part->verts[t.v[2]];

			Vec3 n0 = part->norms[t.n[0]];
			Vec3 n1 = part->norms[t.n[1]];
			Vec3 n2 = part->norms[t.n[2]];
			
			glNormal3f(n0[0], n0[1], n0[2]);
			glVertex3f(v0[0], v0[1], v0[2]);
			
			glNormal3f(n1[0], n1[1], n1[2]);
			glVertex3f(v1[0], v1[1], v1[2]);

			glNormal3f(n2[0], n2[1], n2[2]);
			glVertex3f(v2[0], v2[1], v2[2]);
		}
		glEnd();
	}
}

/////////////////////////////////////////////////

void Engine::drawLight(AreaLightPtr l)
{
	assert(l != 0);
	Vec3 dif = l->getDiffuse();
	Vec3 v0 = l->getVert(0);
	Vec3 v1 = l->getVert(1);
	Vec3 v2 = l->getVert(2);
	Vec3 v3 = l->getVert(3);
	glColor3f(dif[0], dif[1], dif[2]); 
	glBegin(GL_TRIANGLES);
	glVertex3f(v0[0], v0[1], v0[2]);
	glVertex3f(v1[0], v1[1], v1[2]);
	glVertex3f(v2[0], v2[1], v2[2]);

	glVertex3f(v2[0], v2[1], v2[2]);
	glVertex3f(v3[0], v3[1], v3[2]);
	glVertex3f(v0[0], v0[1], v0[2]);
	glEnd();
}

/////////////////////////////////////////////////

void Engine::drawFullScreenQuad()
{
	glActiveTexture(GL_TEXTURE0);
	glBegin(GL_QUADS);
	// When using TEXTURE_RECTANGLE_NV, tex coords are not normalized 
	glTexCoord2i(0, 0);
	glVertex3f(-1.0, -1.0, 0.0);
	
	glTexCoord2i(_size[0], 0);
	glVertex3f(1.0, -1.0, 0.0);
	
	glTexCoord2i(_size[0], _size[1]);
	glVertex3f(1.0, 1.0, 0.0);
	
	glTexCoord2i(0, _size[1]);
	glVertex3f(-1.0, 1.0, 0.0);
	glEnd();
	checkGLError();
}

